/**
 * AjLog - The AspectJ based logging facade
 * 
 * Copyright (C) 2009 - 2010, Marko Umek. All rights reserved.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.ajlog.core.message;

import org.ajlog.util.AjLogLogger;

/**
 * MessageParser is responsible for parsing the message content:
 * <ul>
 * <li>Replacing %n - to the content line.separator from
 * {@link System#getProperties()}</li>
 * <li>Evaluating the escape character (backslash)</li>
 * </ul>
 * 
 * @author Marko Umek
 * 
 */
public class MessageParser {

    private static AjLogLogger  logger           = AjLogLogger.getLogger(MessageParser.class);
    /**
     * The escape character
     */
    private static final char   ESCAPE_CHAR      = '\\';

    /**
     * The newline pattern %n
     */
    private static final String NEWLINE_PATTERN;

    /**
     * The first newline character
     */
    private static final char   NEWLINE_1ST_CHAR = '%';
    /**
     * The second newline character
     */
    private static final char   NEWLINE_2ND_CHAR = 'n';
    /**
     * The first character of ${..} (variable evaluation)
     */
    private static final char   VAR_1ST_CHAR     = '$';

    /**
     * The second character of ${..} (variable evaluation)
     */
    private static final char   VAR_2ND_CHAR     = '{';

    /**
     * The last character of ${..} (variable evaluation)
     */
    private static final char   VAR_LAST_CHAR    = '}';

    /**
     * The variable string without any variable name (of course is abnormal)
     */
    private static final String VAR_STRING       = "${}";

    static {
        NEWLINE_PATTERN = new String(new char[] { NEWLINE_1ST_CHAR, NEWLINE_2ND_CHAR });
    }

    /**
     * Constructor
     * 
     */
    public MessageParser() {
    }

    /**
     * parseMessage is responsible for parsing the message
     * 
     * @param message
     *            the message
     */
    public void parseMessage(final String message, final IMessageParserResultCollector parserResultCollector) {
        final char[] str = message.toCharArray();
        StringBuilder messageBuilder = new StringBuilder("");

        // Start parsing the message string
        logger.debug("Start evaluating '%s' (length=%d)", message, message.length());
        int remainingLength = str.length;
        for (int offset = 0, len = str.length; offset < len;) {
            final char currentChar = str[offset];
            final boolean hasNextChar = offset + 1 < len;
            final char nextChar = hasNextChar ? str[offset + 1] : '\0';

            // %n -> \n
            if (this.isNewline(currentChar, nextChar)) {
                offset += this.replaceNewline(messageBuilder);
            }
            else if (this.isStartOfVar(currentChar, nextChar)) {
                final String messagePart = String.copyValueOf(str, offset, remainingLength);
                final VariableEvaluationResult evalResult = this.evaluatePotentialVariable(messagePart, parserResultCollector);
                if (evalResult.foundVariable) {
                    logger.debug("Variable has been found in '%s' -> skip %d characters", messagePart,
                            evalResult.numberOfCharacters);
                    offset += evalResult.numberOfCharacters;
                    // Push the characters read to the fixed part.
                    parserResultCollector.appendFixedMessage(messageBuilder.toString());
                    // .. and create a clean message builder
                    messageBuilder = new StringBuilder("");
                }
                else {
                    logger.info("No Variable has been found in '%s' -> append %d characters", messagePart,
                            evalResult.numberOfCharacters);
                    messageBuilder.append(String.copyValueOf(str, offset, evalResult.numberOfCharacters));
                    offset += evalResult.numberOfCharacters;
                }

            }
            else if (currentChar == ESCAPE_CHAR) {
                if (hasNextChar && this.isInEscapeableCharacter(nextChar)) {
                    logger.info("'%c' is an escapeable character", nextChar);
                    messageBuilder.append(nextChar);
                    offset += 2;
                }
                else {
                    messageBuilder.append(currentChar);
                    offset++;
                }
            }
            else {
                messageBuilder.append(currentChar);
                offset++;
            }

            remainingLength = len - offset;
        }

        parserResultCollector.appendFixedMessage(messageBuilder.toString());

    }

    /**
     * replaceNewline replaces newline if {@link #isNewline(char, char)} returns
     * <code>true</code>.
     * 
     * @return the next offset
     */
    private int replaceNewline(final StringBuilder messageBuilder) {
        messageBuilder.append(System.getProperty("line.separator"));
        return NEWLINE_PATTERN.length();
    }

    /**
     * isStartOfVar return <code>true</code> if the next two characters are ${
     */
    private boolean isStartOfVar(final char currentChar, final char nextChar) {
        return currentChar == VAR_1ST_CHAR && nextChar == VAR_2ND_CHAR;
    }

    /**
     * isNewline returns true if the next two characters are %n.
     */
    private boolean isNewline(final char currentChar, final char nextChar) {
        return currentChar == NEWLINE_1ST_CHAR && nextChar == NEWLINE_2ND_CHAR;
    }

    /**
     * VariableEvaluationResult contains the result of
     * {@link MessageParser#evaluatePotentialVariable(String, IMessageParserResultCollector)}
     * 
     */
    private static class VariableEvaluationResult {
        boolean foundVariable      = false;
        int     numberOfCharacters = 0;
    }

    /**
     * evaluatePotentialVariable is responsible for evaluating message variable
     * "${...}"
     * 
     * @param messagePart
     * @param parserResultCollector
     * @return
     */
    private VariableEvaluationResult evaluatePotentialVariable(final String messagePart,
            final IMessageParserResultCollector parserResultCollector) {
        final VariableEvaluationResult result = new VariableEvaluationResult();

        // Abnormal case: Is it -> ${} no variable at all
        if (messagePart.startsWith(VAR_STRING)) {
            logger.info("Variable evaluation: Abnormal variable found %s in '%s'", VAR_STRING, messagePart);
            result.foundVariable = false;
            result.numberOfCharacters = VAR_STRING.length();
            return result;
        }

        // Too small: At least VAR_STRING.length should be there
        if (!(messagePart.length() > VAR_STRING.length())) {
            logger.info("Variable evaluation: Message part is too small (%s)", messagePart);
            result.foundVariable = false;
            result.numberOfCharacters = messagePart.length();
            return result;
        }

        final char[] str = messagePart.toCharArray();
        boolean found = false;
        final StringBuilder potentialVariable = new StringBuilder();
        int offset = VAR_STRING.length() - 1;
        for (final int len = str.length; offset < len;) {
            final char currentChar = str[offset];
            if (currentChar == VAR_LAST_CHAR) {
                found = true;
                break;
            }

            final boolean hasNextChar = offset + 1 < len;
            final char nextChar = hasNextChar ? str[offset + 1] : '\0';
            // handle \}, \%
            if (currentChar == ESCAPE_CHAR && this.isInEscapeableCharacterInVar(nextChar)) {
                logger.debug("Variable evaluation: Skip \\%c", nextChar);
                potentialVariable.append(nextChar);
                offset += 2;
            }
            else if (this.isNewline(currentChar, nextChar)) {
                offset += this.replaceNewline(potentialVariable);
            }
            else {
                potentialVariable.append(currentChar);
                offset++;
            }
        }

        final String variable = potentialVariable.toString();
        if (found) {
            result.foundVariable = parserResultCollector.appendVariable(variable);
            result.numberOfCharacters = offset + 1;
        }
        else {
            logger.warn(
                    "Variable evaluation: Couldn't found the closing bracket in '%s' -> So no variable could be determined",
                    messagePart);
            result.foundVariable = false;
            result.numberOfCharacters = VAR_STRING.length() - 1;
        }

        return result;
    }

    private boolean isInEscapeableCharacterInVar(final char nextChar) {
        if (nextChar == VAR_LAST_CHAR) {
            return true;
        }
        if (nextChar == NEWLINE_1ST_CHAR) {
            return true;
        }
        return false;
    }

    /**
     * isInEscapebleCharacter returns <code>true</code> if the next character
     * should be escaped or not.
     */
    private boolean isInEscapeableCharacter(final char nextChar) {
        if (nextChar == NEWLINE_1ST_CHAR) {
            return true;
        }
        if (nextChar == ESCAPE_CHAR) {
            return true;
        }
        if (nextChar == VAR_1ST_CHAR) {
            return true;
        }
        return false;
    }
}
